﻿//#define VERBOSE
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Entity.Frames;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Types;

namespace RayDen.RayEngine.Data
{
    using RayInfo = RayData;


    public interface IGeoLightsource
    {
        void Initialize(IRayEngineScene scn, params object[] data);
        void Le(ref Vector w, out float pdf, ref RgbSpectrum spd);
    }



    public class MeshLight : ILight, IGeoLightsource
    {
        struct TriangleSample
        {
            public Normal Item2;
            public float Item1;

            public TriangleSample(float i1, Normal i2)
            {
                Item1 = i1;
                Item2 = i2;
            }
        }

        public const float NormalModifier = 1.0f;
        public string LightName;
        protected internal ITriangleMesh mesh;
        private RayEngineScene scene;


        private ISpectrum zeroSpectra;
        public RgbSpectrum gain;
        public RgbSpectrumInfo infoGain;

        public SampledSpectrum spectra;

        private FastRandom rnd = new FastRandom();
        private float meshMeshArea;
        public string MeshName;
        private static SampledSpectrum ZeroCSpectrumArray = SampledSpectrum.ZeroSpectrum();
        private RgbSpectrumInfo RgbSpectrumZeroArray = RgbSpectrumInfo.ZeroSpectrum();
        private TriangleSample[] triangleSampleData;

        private float[] lightSpectra;
        public string Name
        {
            get { return MeshName; }
        }
        public EmissionProfile Profile { get; set; }

        public MeshLight() { }

        public MeshLight(FrameLightsource lightsource)
        {
            MeshName = lightsource.Parameters.Get<string>(FrameLightsource.AreaLightGeometryName);
            LightName = MeshName;
            spectra = SPD_Data.FromSampled(SPD_Data.D5000, SpectrumType.Illuminant);//FromFile(@"F:\3D\spds\cie.stdillum.F10.spd")
            gain = lightsource.Parameters.Get<RgbSpectrum>(FrameLightsource.LightsourcePower);
            infoGain = new RgbSpectrumInfo(gain);
        }

        public RgbSpectrumInfo Emittance(ref Vector dir, out float pdf)
        {
            var tr = triangleSampleData[SampleTriangleIndex()];

            var sampleN = tr.Item2;
            pdf = 1f / triangleSampleData.Length * (1f / tr.Item1);
            if ((double)Normal.Dot(ref sampleN, ref dir) <= 0.0)
                return new RgbSpectrumInfo(0.0f);
            else
                return infoGain;
        }
        public void Initialize(IRayEngineScene scen, params object[] data)
        {
            zeroSpectra = GlobalConfiguration.Instance.SpectralRendering
                            ? (ISpectrum)ZeroCSpectrumArray
                            : this.RgbSpectrumZeroArray;
            var scn = (RayEngineScene)scen;
            if (mesh == null && !string.IsNullOrWhiteSpace(MeshName))
            {

                this.mesh =
                    scn.Meshes.FirstOrDefault(
                        item => item.MeshName.Equals(MeshName, StringComparison.CurrentCultureIgnoreCase));
                if (this.mesh != null)
                {
                    if (this.mesh.MeshProfile == null)
                    {
                        this.mesh.MeshProfile = new LightsourceProfile()
                        {
                            Light = this
                        };
                    }
                    else
                    {
                        var lightsourceProfile = this.mesh.MeshProfile as LightsourceProfile;
                        if (lightsourceProfile != null)
                            lightsourceProfile.Light = this;
                    }
                    meshMeshArea = 0;
                    for (int i = mesh.StartTriangle; i < mesh.EndTriangle; i++)
                    {
                        meshMeshArea += scn.Triangles[i].AreaV(scn.Vertices);
                    }
                }
            }


            if (mesh == null)
            {
                throw new ArgumentException("Cant find light mesh", this.LightName ?? this.MeshName);
            }

            triangleSampleData = new TriangleSample[this.mesh.TrianglesCount];
            this.scene = scn;

            for (int i = mesh.StartTriangle, j = 0; i < mesh.EndTriangle; i++, j++)
            {
                triangleSampleData[j] = new TriangleSample(scene.Triangles[i].AreaV(scene.Vertices), NormalModifier * scene.Triangles[i].ComputeNormal(scene.Vertices));
            }

            triangleSampleData.PartialSort((a, b) => a.Item1.CompareTo(a.Item1), 0, triangleSampleData.Length);

            if (gain.IsBlack() || infoGain == null)
            {
                gain = scn.DefaultLightGain;
                infoGain = new RgbSpectrumInfo(gain);
            }
            lightSpectra = GlobalConfiguration.Instance.SpectralRendering ? spectra.ToArray() : gain.ToArray();
        }

        protected TriangleDataInfo SampleTriangle(float f, out int index)
        {
            index = Math.Max(0, Math.Min(triangleSampleData.Length - 1, (int)(f * (mesh.EndTriangle - mesh.StartTriangle))));

            return scene.Triangles[mesh.StartTriangle + index];
        }

        private int SampleTriangleIndex()
        {
            return rnd.Next(0, triangleSampleData.Length - 1);
        }

        #region ILight Members

        public bool IsDelta
        {
            get
            {
                return false;
            }
        }

        public ISpectrum Le(ref Vector wo)
        {
            var sampleN = triangleSampleData[SampleTriangleIndex()].Item2;

            if (Normal.Dot(ref sampleN, ref wo) <= 0f)
            {
                return GlobalConfiguration.Instance.SpectralRendering ? (ISpectrum)ZeroCSpectrumArray : this.RgbSpectrumZeroArray;
            }

            return GlobalConfiguration.Instance.SpectralRendering ? (ISpectrum)spectra : infoGain; // Light sources are supposed to have flat color
        }

        public void Le(ref Vector dir, ref ISpectrum le)
        {
            var sampleN = triangleSampleData[SampleTriangleIndex()].Item2;
            //NormalModifier * SampleTriangle().ComputeNormal(scene.Vertices);

            if (Normal.Dot(ref sampleN, ref dir) <= 0f)
            {
                le.Mul(0f);
            }

            var s = GlobalConfiguration.Instance.SpectralRendering ? (ISpectrum)spectra : infoGain;
            le.Mul(ref s);
        }

        public void Le(ref Vector wo, out float pdf, ref RgbSpectrum spd)
        {
            var ti = SampleTriangleIndex();
            var sampleN = triangleSampleData[ti].Item2;
            var area = triangleSampleData[ti].Item1;
            pdf = (1f / area) * (1f / mesh.TrianglesCount);
            if (Normal.Dot(ref sampleN, ref wo) <= 0f)
            {
                spd.Mul(0f);
                return;
            }

            var s = GlobalConfiguration.Instance.SpectralRendering ? (ISpectrum)spectra : gain; // Light sources are supposed to have flat color
            spd.Mul(ref s);

        }

        //public void Emit(float u0, float u1, float u2, float u3, float passThroughEvent,
        //    out Point orig, out Vector dir,
        //    out float emissionPdfW, out float directPdfA, out float cosThetaAtLight)
        //{

        //}
        public void SampleSpatialDirect(ref Point point, ref Normal n, float u0, float u1, float u2, ref LightSample ls)
        {
            int tries = 0;

#if VERBOSE
            try
            {
#endif
                var samplePoint = new Point();
                float b0, b1, b2;
                int maxTries = 1;
            startTry:

                var index = Math.Max(0, Math.Min(triangleSampleData.Length - 1, (int)(u0 * (mesh.EndTriangle - mesh.StartTriangle))));
                Sample(ref scene.Triangles[mesh.StartTriangle + index], scene.Vertices, u2, u1, out samplePoint, out b0, out b1, out b2);

                ls.U = b0;
                ls.V = b1;

                var triangleNormal = triangleSampleData[index].Item2;
                var area = triangleSampleData[index].Item1;
                //tri.AreaV(scene.Vertices);
                //var sampleN = TriangleNormal;
                //var N = n;

                Vector wi = samplePoint - point;
                //wi.Normalize();
                float distanceSquared = wi.Length2();
                ls.Distance = MathLab.Sqrt(distanceSquared);
                wi /= ls.Distance;

                var nw = -wi;
                float sampleNdotMinusWi = Normal.Dot(ref triangleNormal, ref nw);
                float NdotWi = Normal.Dot(ref n, ref wi);
                if ((sampleNdotMinusWi <= 0f) || (NdotWi <= 0f))
                {
                    tries++;
                    if (tries > maxTries)
                    {
                        ls.Pdf = 0f;
                        return;
                    }

                    goto startTry;
                }

                ls.Pdf =
                    //((1f / area) * distanceSquared / sampleNdotMinusWi) * (1f / mesh.TrianglesCount) * (1f / Math.Max(1, tries + 1));
                    (distanceSquared / (sampleNdotMinusWi * area)) * 1f / mesh.TrianglesCount * (1f / Math.Max(1, tries + 1));




                // Using 0.01 instead of 0.0 to cut down fireflies
                if (ls.Pdf <= 0.01f)
                {
                    ls.Pdf = 0f;
                    return;
                }
                ls.LightRay.Dir = wi;
                //, MathLab.RAY_EPSILON, distance - MathLab.RAY_EPSILON);
#if VERBOSE
            }
            catch (Exception ex)
            {
                Tracer.TraceLine(ex.Message + ex.StackTrace);
                Tracer.TraceLine("Triangle data offset " + (mesh.StartTriangle).ToString() + "of " + triangleSampleData.Length);
                throw ex;
            }
#endif
        }


        public void SampleSpatialAdjoint(IRayEngineScene scen, float u0, float u1, float u2, float u3, float u4,
            out Vector dir, out float apdf, out float pdf)
        {
            var scene = (RayEngineScene)scen;
            float b0, b1, b2;
            Point orig;
            int index;
            var tri = SampleTriangle(u4, out index);

            tri.Sample(scene.Vertices, u0, u1, out orig, out b0, out b1, out b2);
            var TriangleNormal = triangleSampleData[index].Item2;
            var area = triangleSampleData[index].Item1;
            // Ray direction
            var sampleN = -TriangleNormal;
            dir = MC.UniformSampleSphere(u2, u3);
            float rdotN = Normal.Dot(ref dir, ref sampleN);
            if (rdotN < 0f)
            {
                dir *= -1f;
                rdotN = -rdotN;
            }
            apdf = rdotN;

            pdf = ((MathLab.INVTWOPI / area) * (1f / mesh.TrianglesCount));

        }
        public void EvaluateShadow(ref Point point, ref Normal n, float u0, float u1, float u2, ref LightSample result)
        {
            int tries = 0;

#if VERBOSE
            try
            {
#endif
                if (result == null)
                    result = new LightSample();

                var samplePoint = new Point();
                float b0, b1, b2;
                int maxTries = 1;
            startTry:

                var index = Math.Max(0, Math.Min(triangleSampleData.Length - 1, (int)(u0 * (mesh.EndTriangle - mesh.StartTriangle))));
                scene.Triangles[mesh.StartTriangle + index].Sample(scene.Vertices, u2, u1, out samplePoint, out b0, out b1, out b2);
                //TriangleDataInfo.Sample(ref scene.Triangles[mesh.StartTriangle + index], scene.Vertices, u2, u1, out samplePoint, out b0, out b1, out b2);
                var area = triangleSampleData[index].Item1;
                //tri.AreaV(scene.Vertices);
                //var sampleN = TriangleNormal;
                //var N = n;

                Vector wi = samplePoint - point;
                //wi.Normalize();
                float distanceSquared = wi.Length2();
                float distance = MathLab.Sqrt(distanceSquared);
                wi /= distance;

                var nw = -wi;
                float sampleNdotMinusWi = Normal.Dot(ref triangleSampleData[index].Item2, ref nw);
                float NdotWi = Normal.Dot(ref n, ref wi);
                if ((sampleNdotMinusWi <= 0f) || (NdotWi <= 0f))
                {
                    tries++;
                    if (tries > maxTries)
                    {
                        return;
                    }

                    goto startTry;
                }

                result.Pdf =
                    //((1f / area) * distanceSquared / sampleNdotMinusWi) * (1f / mesh.TrianglesCount) * (1f / Math.Max(1, tries + 1));
                    (distanceSquared / (sampleNdotMinusWi * area)) * 1f / mesh.TrianglesCount * (1f / Math.Max(1, tries + 1));




                // Using 0.01 instead of 0.0 to cut down fireflies
                if (result.Pdf <= 0.01f)
                {
                    result.Pdf = 0f;
                    result.Spectrum = GlobalConfiguration.Instance.SpectralRendering
                        ? (ISpectrum)ZeroCSpectrumArray
                        : this.RgbSpectrumZeroArray;
                    return;
                }
                result.LightRay = new RayInfo(point, wi, 1e-4f, 1e3f);
                //, MathLab.RAY_EPSILON, distance - MathLab.RAY_EPSILON);
                result.Distance = distance;
                result.Spectrum = Profile.Evaluate(b0, b1);
#if VERBOSE
            }
            catch (Exception ex)
            {
                Tracer.TraceLine(ex.Message + ex.StackTrace);
                Tracer.TraceLine("Triangle data offset " + (mesh.StartTriangle).ToString() + "of " + triangleSampleData.Length);
                throw ex;
            }
#endif
        }

        public void EvaluatePhoton(IRayEngineScene scen, float u0, float u1, float u2, float u3, float u4, out LightSample result)
        {
            result = new LightSample();
            var scene = (RayEngineScene)scen;
            float b0, b1, b2;
            Point orig;
            int index;
            var tri = SampleTriangle(u4, out index);

            tri.Sample(scene.Vertices, u0, u1, out orig, out b0, out b1, out b2);
            var TriangleNormal = triangleSampleData[index].Item2;
            var area = triangleSampleData[index].Item1;
            // Ray direction
            var sampleN = -TriangleNormal;
            Vector dir = MC.UniformSampleSphere(u2, u3);
            float rdotN = Normal.Dot(ref dir, ref sampleN);
            if (rdotN < 0f)
            {
                dir *= -1f;
                rdotN = -rdotN;
            }

            result.LightRay = new RayInfo(ref orig, ref dir);


            result.Pdf = ((MathLab.INVTWOPI / area) * (1f / mesh.TrianglesCount));

            result.Spectrum = Profile.Evaluate(b0, b1);
            result.Spectrum.Mul(rdotN);
        }

        #endregion


        public static void Sample(ref TriangleDataInfo tri, Point[] meshVertices, float u0, float u1, out Point samplePoint, out float b0, out float b1, out float b2)
        {
            b0 = 0f;
            b1 = 0f;
            MC.UniformSampleTriangle(u0, u1, ref  b0, ref b1);

            var p0 = meshVertices[tri.v0.VertexIndex];
            var p1 = meshVertices[tri.v1.VertexIndex] - p0;
            var p2 = meshVertices[tri.v2.VertexIndex] - p0;
            b2 = 1f - (b0) - (b1);


            samplePoint = new Point(
                ((b0) * p0.x + (b1) * p1.x + (b2) * p2.x),
                ((b0) * p0.y + (b1) * p1.y + (b2) * p2.y),
                ((b0) * p0.z + (b1) * p1.z + (b2) * p2.z));
        }
    }
}

